package cn.selfiot.iotman.view.editor;

import cn.selfiot.iotman.core.I18N;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Cursor;
import javafx.scene.control.*;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Paint;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.model.StyleSpans;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URL;
import java.util.Collection;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * @author hugo
 * @since 2020/8/5
 */
public class JsonEditorController implements Initializable {
    private static final Logger LOGGER = LoggerFactory.getLogger(JsonEditorController.class);
    
    @FXML
    public VBox root;
    @FXML
    public HBox hbToolBar;
    @FXML
    public Label lbStatusMsg;
    @FXML
    public Button btnFormat;
    @FXML
    public Button btnClear;
    @FXML
    public ToggleButton btnToggle;
    @FXML
    public SplitPane spBody;
    @FXML
    public VBox vbTreeView;
    @FXML
    public VBox vbEditor;
    @FXML
    public TreeTableView treeViewer;
    
    private ResourceBundle bundle;
    private StyleClassedTextArea codeArea;
    private boolean onlyEditor = false;
    private Parser mParser;
    private ExecutorService executorService;
    private Future mParsingFuture;
    private final ChangeListener<String> contentChangeListener = (obs, oldText, newText) -> parseCode(newText);
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        bundle = I18N.getBundle(EditorConstants.BUNDLE);
        
        executorService = Executors.newFixedThreadPool(2);
        mParser = new JsonParser();
        
        collapse();
        VBox.setVgrow(root, Priority.ALWAYS);
        
        initCodeArea();
        initTreeView();
        initAction();
    }
    
    public void setOnlyEditor() {
        this.onlyEditor = true;
        codeArea.setLineHighlighterOn(false);
        codeArea.setEditable(false);
        hbToolBar.setVisible(false);
        hbToolBar.setManaged(false);
        
        treeViewer.setVisible(false);
        treeViewer.setManaged(false);
    }
    
    private void initCodeArea() {
        codeArea = new StyleClassedTextArea();
        codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
        codeArea.prefWidthProperty().bind(vbEditor.widthProperty());
        codeArea.textProperty().addListener(contentChangeListener);
        codeArea.setLineHighlighterOn(true);
        codeArea.setEditable(!onlyEditor);
        codeArea.setCursor(Cursor.DEFAULT);
        VBox.setVgrow(codeArea, Priority.ALWAYS);
        
        VirtualizedScrollPane scroll = new VirtualizedScrollPane(codeArea);
        VBox.setVgrow(scroll, Priority.ALWAYS);
        vbEditor.getChildren().add(scroll);
        
        codeArea.getStylesheets().clear();
        codeArea.getStylesheets().add(mParser.getStylesheets());
    
        ContextMenu mEditorContextMenu = createContextMenu();
        codeArea.setContextMenu(mEditorContextMenu);
        codeArea.setOnContextMenuRequested(event -> {
            codeArea.getContextMenu().show(codeArea.getScene().getWindow());
            event.consume();
        });
    }
    
    private void initTreeView() {
        treeViewer.setEditable(false);
        treeViewer.getColumns().clear();
        treeViewer.getColumns().addAll(mParser.getTreeTableViewColumns());
        treeViewer.setRowFactory(param -> new EditorTreeTableRow(bundle));
    }
    
    private void initAction() {
        btnFormat.setOnAction(actionEvent -> formatCode());
        btnClear.setOnAction(actionEvent -> clearContent());
        btnToggle.setOnAction(actionEvent -> {
            boolean selected = btnToggle.isSelected();
            if (selected) {
                expand();
            } else {
                collapse();
            }
        });
    }
    
    private void formatCode() {
        String code = codeArea.getText();
        if (code == null || code.length() == 0) {
            LOGGER.warn("content empty!");
        } else {
            try {
                code = mParser.prettyPrint(code);
            } catch (Exception e) {
                LOGGER.error(e.getMessage());
            }
            codeArea.textProperty().removeListener(contentChangeListener);
            codeArea.replaceText(code);
            codeArea.setStyleSpans(0, mParser.computeHighlighting(code));
            codeArea.textProperty().addListener(contentChangeListener);
        }
    }
    
    private void parseCode(String code) {
        if (mParsingFuture != null && !mParsingFuture.isDone()) {
            mParsingFuture.cancel(true);
        }
        
        mParsingFuture = executorService.submit(() -> {
            final StyleSpans<Collection<String>> spans = mParser.computeHighlighting(code);
            Platform.runLater(() -> {
                try {
                    codeArea.setStyleSpans(0, spans);
                } catch (Exception e) {
                    LOGGER.error(e.getMessage());
                }
            });
            
            TreeItem rootTreeItem;
            try {
                rootTreeItem = mParser.parseCode(code);
                Platform.runLater(() -> {
                    treeViewer.setRoot(rootTreeItem);
                    setStatusMessage(bundle.getString("ready"), Paint.valueOf("green"));
                });
            } catch (Exception e) {
                Platform.runLater(() -> {
                    treeViewer.setRoot(null);
                    if (code.isEmpty()) {
                        setStatusMessage(bundle.getString("ready"), Paint.valueOf("green"));
                    } else {
                        setStatusMessage(e.getLocalizedMessage(), Paint.valueOf("red"));
                    }
                });
            }
        });
    }
    
    private void setStatusMessage(String message, Paint color) {
        lbStatusMsg.setText(message);
        lbStatusMsg.setTextFill(color);
    }
    
    private ContextMenu createContextMenu() {
        ContextMenu menu = new ContextMenu();
        
        MenuItem itemCopy = new MenuItem(bundle.getString("copy"));
        itemCopy.setAccelerator(KeyCombination.keyCombination("CTRL+C"));
        itemCopy.setOnAction(event -> {
            codeArea.copy();
            event.consume();
        });
        
        MenuItem itemCut = new MenuItem(bundle.getString("cut"));
        itemCut.setAccelerator(KeyCombination.keyCombination("CTRL+X"));
        itemCut.setOnAction(event -> {
            codeArea.cut();
            event.consume();
        });
        
        MenuItem itemPaste = new MenuItem(bundle.getString("paste"));
        itemPaste.setAccelerator(KeyCombination.keyCombination("CTRL+V"));
        itemPaste.setOnAction(event -> {
            codeArea.paste();
            event.consume();
        });
        
        MenuItem itemPrettyPrint = new MenuItem(bundle.getString("format"));
        itemPrettyPrint.setAccelerator(KeyCombination.keyCombination("CTRL+SPACE"));
        itemPrettyPrint.setOnAction(event -> {
            String code = codeArea.getText();
            try {
                code = mParser.prettyPrint(code);
            } catch (Exception e) {
                LOGGER.error(e.getMessage());
            }
            
            codeArea.textProperty().removeListener(contentChangeListener);
            codeArea.replaceText(code);
            codeArea.setStyleSpans(0, mParser.computeHighlighting(code));
            codeArea.textProperty().addListener(contentChangeListener);
            event.consume();
        });
        
        menu.getItems().addAll(itemCopy, itemCut, itemPaste, new SeparatorMenuItem(), itemPrettyPrint);
        return menu;
    }
    
    void shutDown() {
        try {
            executorService.shutdown();
            executorService.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {
            LOGGER.error(ex.getMessage());
        }
    }
    
    private void clearContent() {
        codeArea.clear();
    }
    
    private void collapse() {
        spBody.setDividerPositions(1, 0);
    }
    
    private void expand() {
        spBody.setDividerPositions(0.7, 0.5);
    }
    
    public ToggleButton getBtnToggle() {
        return btnToggle;
    }
    
    public SplitPane getSpBody() {
        return spBody;
    }
    
    public StyleClassedTextArea getCodeArea() {
        return codeArea;
    }
}
